/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the ViewMyCurrency extension.
 *
 * The Initial Developer of the Original Code is
 * Will Moffat.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

//dump("// $Id: WILL_NumberConversion.js,v 1.53 2007/03/01 11:39:56 Will Exp $;\n");

VMC.Number = function(text) {
  if (arguments[0] && arguments[0]=='empty') { return this; } // for testbench

  // private vars (actually public)
  this._origStr = null;     // string representation of this number (in original format)
  this._origLen = 0;        // and length of that string
  this._origNum = NaN;      // float  representation of this number
  this._origNumSigFigs=NaN; // Original number of significant figures
  this._origNumSigDecs=NaN; // Original number of significant decimals (-ve number) //WILL: is this used??

  this._newNum  = NaN;      // either the orignal number or the original number multiplied by a rate

  this._decimalSeparator   = null;
  this._thousandsSeparator = null;

  this._init(text);

  return this; //WILL: TODO: is this necessary (matches // for testbench)
};

VMC.Number.prototype = {
  thousandsSeparator : ',',
  decimalSeparator   : '.',    //default settings for numbers, overridden by preferences

  POINT : '.',
  COMMA : ',',
  SPACE : ' ',
  QUOTE : "'",
  BLANK : '',

  POINT_RE : /\./g,
  COMMA_RE : /\,/g,
  SPACE_RE : /\s/g,
  QUOTE_RE : /\'/g,

  findLongestNumber : function(textStr) {
  // textStr should start with a number hence leading ^
  var thousandsSpace_OptionalDecimal_RE = /^\d{1,3}(?:\s\d\d\d)*(?:[,.]\d+)?/; // 1-3 digits (space 3 digits)* (comma|dot digits+)?
  var thousandsComma_OptionalDecimal_RE = /^\d{1,3}(?:\,\d\d\d)*(?:\.\d+)?/;   // 1-3 digits (comma 3 digits)* (dot       digits+)?
  var thousandsPoint_OptionalDecimal_RE = /^\d{1,3}(?:\.\d\d\d)*(?:\,\d+)?/;   // 1-3 digits (point 3 digits)* (comma     digits+)?
  var thousandsQuote_OptionalDecimal_RE = /^\d{1,3}(?:\'\d\d\d)*(?:\.\d+)?/;   // 1-3 digits (quote 3 digits)* (dot       digits+)?
  var thousandsPlain_OptionalDecimal_RE = /^\d+(?:[,.]\d+)?/;                  // 1 or more digits (comma|dot digits+)?

  var r;
  var regexps = [ thousandsSpace_OptionalDecimal_RE,
		  thousandsComma_OptionalDecimal_RE,
		  thousandsPoint_OptionalDecimal_RE,
		  thousandsQuote_OptionalDecimal_RE,
		  thousandsPlain_OptionalDecimal_RE ];
    
  var numberStr='';
  for (var i=0;i<regexps.length;++i) {
    r=regexps[i].exec(textStr);
    if (r && r[0].length > numberStr.length) {
      numberStr=r[0];
    }
  }
  
  if (!numberStr) { throw 'Cannot create number from '+textStr; }
  //VMC.log('DBG','Found number ['+numberStr+']');
  return numberStr;
  },

  // private funcs
  _init : function(text) {

    var number = this.findLongestNumber(text);

    this._origStr = number;
    this._origLen = number.length;

    this._parseString(number);
    // Sets the following:
    // this._origNum and this._newNum
    // this._origNumSigFigs
    // this._origNumSigDecs
  },

  // convert into a Javascript formatted number and store as float
  _parseString : function(numStr) {

    this.detectSeparators(numStr);

    // remove thousands separators and use POINT as decimal separator
    switch (this._thousandsSeparator) {
    case this.BLANK:
      break;
    case this.COMMA:
      numStr = numStr.replace(this.COMMA_RE,'');
      break;
    case this.SPACE:
      numStr = numStr.replace(this.SPACE_RE,'');
      break;
    case this.QUOTE:
      numStr = numStr.replace(this.QUOTE_RE,'');
      break;
    case this.POINT:
      numStr = numStr.replace(this.POINT_RE,'');
      break;
    default:
      throw 'Unknown thousands separator '+this._thousandsSeparator;
    }

    switch (this._decimalSeparator) {
    case this.COMMA:
      numStr = numStr.replace(this.COMMA_RE,this.POINT); // POINT as decimal
      break;
    case this.POINT:      
      break; //do nothing
    default:
      throw 'Unknown decimal separator '+this._decimalSeparator;
    }

    this._origNum=parseFloat(numStr);
    this._newNum = this._origNum;

    var sigfig = new VMC.SignificantFigures(numStr); //WILL: TODO: possible optimisation - only generate these if needed
    this._origNumSigFigs = sigfig.sigFigs;
    this._origNumSigDecs = sigfig.sigDecs;
  },


  // helpers functions for dectectSeparators 
  setThousandsStyleBlankUK : function() {
    this._thousandsSeparator = this.BLANK;
    this._decimalSeparator   = this.POINT;
  },
  setThousandsStyleCommaUK : function() {
    this._thousandsSeparator = this.COMMA;
    this._decimalSeparator   = this.POINT;
  },
  setThousandsStyleBlankEU : function() {
    this._thousandsSeparator = this.BLANK;
    this._decimalSeparator   = this.COMMA;
  },
  setThousandsStylePointEU : function() {
    this._thousandsSeparator = this.POINT;
    this._decimalSeparator   = this.COMMA;
  },
  setThousandsStyleQuote : function() {
    this._thousandsSeparator = this.QUOTE;
    this._decimalSeparator   = this.POINT;
  },
  setThousandsStyleSpaceSwiss : function(decSep) {
    this._thousandsSeparator = this.SPACE;
    this._decimalSeparator   = decSep;
  },
  

  // sets this._thousandsSeparator, this._decimalSeparator
  detectSeparators : function(number_str) {
    // Try and guess how this number is formatted
    var commas=0;
    var points=0;
    var spaces=0;
    var quotes=0;
    var last_symbol=null;

    for (var i=0;i<number_str.length;++i) {
      var c=number_str[i];
      if (c==this.COMMA ) { ++commas; }
      if (c==this.POINT ) { ++points; }
      if (c==this.SPACE ) { ++spaces; }
      if (c==this.QUOTE ) { ++quotes; }
    }
    
    if (spaces>0) {
      if         (commas==1 && !points) {
	return this.setThousandsStyleSpaceSwiss(this.COMMA); // spaces seperated thousands, decimal separator=COMMA
      } else  if (points==1 && !commas) {
	return this.setThousandsStyleSpaceSwiss(this.POINT); // spaces seperated thousands, decimal separator=POINT
      } else  if (!commas && !points) {
	return this.setThousandsStyleSpaceSwiss(this.POINT); // spaces seperated thousands, decimal separator=POINT //WILL: POINT is an arbitary choice!
      }
      throw 'Found number formatted with spaces but cannot parse ['+number_str+']';
    }

    var m=number_str.match(/([\.\,])\d*$/);
    if (m) { last_symbol=m[1]; }

    if (quotes>=1) {
      if (spaces || commas || points >1) {
	throw 'Badly formatted number. If quote is used then  no SPACES, COMMAS  and maximum of one POINT. number='+number_str;
      }
      return this.setThousandsStyleQuote();
    }
  
    if (commas===0 && points===0) { //there is no formatting in this number. Let default to the point symbol.
      return this.setThousandsStyleBlankUK();
    }

    
    if (commas>1  && points>1 ) { throw 'Badly formatted number!  like 123.234.456,346,451 number='+number_str; }
    if (commas==1 && points==1) { //WILL: No error detection for group size
      if (last_symbol==this.POINT) {
	return this.setThousandsStyleCommaUK();
      } else {
	return this.setThousandsStylePointEU();
      }
    }
	
    if (commas==1 && points>1 ) {return this.setThousandsStylePointEU(); } //points must be the thousands separator
    if (commas>1  && points==1) {return this.setThousandsStyleCommaUK(); } //commas must be the thousands separator
    if ( (commas>0 && points===0) || (commas===0 && points>0) ){
      if (number_str[0]=='0') { //if leading digit is a 0: WILL: won't work for negative numbers!
	if (commas==1) {return this.setThousandsStyleBlankEU(); }
	if (points==1) {return this.setThousandsStyleBlankUK(); }
      }
      //we have 123.456.678 or 123,456,789
      //Does 123,456 mean hundreds or hundreds of thousands? --> We guess thousands due to grouping of 3
      var groups=number_str.split(/[\.,]/ , 10);
      var last_group=groups.pop(); //WILL: TODO: error check that all groups (apart from first) are of size 3?
      if (last_group.length==3) {
	//we found the thousands separator - we return the decimal separator
	if (last_symbol==this.POINT) {return this.setThousandsStylePointEU(); }
	if (last_symbol==this.COMMA) {return this.setThousandsStyleCommaUK(); }
      } else {
	//there was not exactly 3 digits in the last group, so it probably wasn't the thousands separator
	if (last_symbol==this.POINT) {return this.setThousandsStyleBlankUK(); }
	if (last_symbol==this.COMMA) {return this.setThousandsStyleBlankEU(); }
      }
    }
    throw 'problem with '+number_str;
    //WILL: JSlint does not like: return undefined;
  },

  
  // public funcs
  multiply : function(rate) {
    if (!rate) { throw "Invalid rate"; }
    this._newNum = this._origNum * rate; // Note that it's always the original number that is multiplied
  },

  // takes '1234567.89' --> '1,234,567.89' 
  // WILL: return string representation of number (currently only in UK/US format)
  // WILL: doesn't work with negative numbers!  
  toString: function() {
    var newNum=this._newNum;
    if (newNum===undefined || newNum===null) { throw "Invalid newNumber"; }
    if (newNum===NaN) { return "???"; }
    if (newNum===0) { return "0"; }
    if (newNum<0.000001) { return "???"; } // deals with too small and negative numbers
   

    var numberStrJavascript = newNum.toString();

    var newNumberOfSigFigs  = this._origNumSigFigs;
    var newMaxDecimals      = -10; //this._origNumSigDecs;

    if (this._origNumSigDecs == -2) {
      // special case - keep format with 2 deciaml places $2.34 --> $5.67 - looks better
      newMaxDecimals=-2;
    }

    if (this.performRounding) {
      // don't show fractional part for numbers greater than 10 (and round the integer part)
      // The maximum error this can introduce is 0.5/10 = 5% error.
      if (newNum>10) { newMaxDecimals=0; } 
    }

    var dirty = (newNum != this._origNum);
    if (dirty) {
      ++newNumberOfSigFigs;
    }

    var numberStr;
    var r;
    var num;
    var fract;
     
    // format with thousands separators. (WILL: TODO: merge with displaySigFigs)
   
    // first check the number of decimal places - if we are dirty (changed) and there is a fractional part and it's 1 digit long then increment its length
    do {
      numberStr=VMC.SignificantFigures.prototype.displaySigFigs(numberStrJavascript,newNumberOfSigFigs,newMaxDecimals,false); //scientific notation = false 
      
      r=numberStr.split(this.POINT,2);
      num=r[0];
      fract=r[1];
      ++newNumberOfSigFigs;
    } while ( dirty && fract && fract.length==1 );


    //Now add the users desired thousandsSeparator and decimalSeparator
    var c=0;
    var str='';
    for (var i=num.length-1;i>0;i--) {
      ++c;
      str=num[i]+str;
      if (c%3===0) {
	str=this.thousandsSeparator+str;
      }
    }
    str=num[0]+str; //add the first digit (it can't be a thousandsSeparator)
    if (fract) { str=str+this.decimalSeparator+fract; } //add the fractional part if it exists
    return str;
  },

  performRounding: true // round numbers > 10     WILL: TODO: make this an option? consider implications for $20.7m 
};

 
////////////////////////////////////////////////////////////////////////////////
//                              NumberConversion                              //
//////////////////////////////////////////////////////////////////////////////// 



VMC.NumberConversion = function() {}; // WILL: TODO: this is just a wrapper to keep all the funcs and vars out of scope. It's not really OOP

VMC.NumberConversion.prototype = {

  currencyLeftRegExp2 : null, //WILL: TODO: this is currently a global var set in VMC.ConvertText
  currencyRightRegExp2 : null,

  TEXT   : 'T', //WILL: TODO: token types should be defined in OOP manner
  SPACE  : 'W',
  SYMBOL : 'S',
  NUM    : 'N',

  WINDOW_SIZE : 20,
  MAX_NUMBER_LEN : 20,



// NB: this only tokenises around numbers!! Not every whitespace and currency symbol is tokenised. Only if it's around a number.
tokeniseNumbers : function(origText) {
  var tokenStream = {};
  tokenStream.tokens=[];
  tokenStream.types=[];
  var numberStr;
  var numberStartIndex;
  var numberEndIndex=0;
  var previousNumberEndIndex=0;
  var r; //for regexp results
  var toSearchStr; // window of text for looking for expanded number
  var leftStr; // text since previous number
  var firstIteration=true;
  // RegExp.input=origText; //WILL: TODO: can we optimise based on this?
  var matchDigit_RE = /\d/g;
  VMC.log('DBG3','looking for digits in ['+origText+']');
  while ( (r=matchDigit_RE.exec(origText)) ) { //find a digit 
    //VMC.log('DBG','Found a digit at '+r[0]);
    numberEndIndex=matchDigit_RE.lastIndex;
    numberStartIndex=numberEndIndex-1;

    // we limit the search area - WILL: TODO: is this necesary - maybe just search right context??
    toSearchStr=origText.substr(numberStartIndex,this.MAX_NUMBER_LEN);

    numberStr = VMC.Number.prototype.findLongestNumber(toSearchStr);

    numberEndIndex=numberStartIndex+numberStr.length;
    matchDigit_RE.lastIndex=numberEndIndex; // manually move the next RegExp starting point since we may have found a longer number 
    leftStr = origText.slice(previousNumberEndIndex,numberStartIndex);
    //debug('left=['+leftStr+']');
    //debug('number=['+numberStr+']');

    this.tokeniseNonNumbers(tokenStream,leftStr,firstIteration);

    tokenStream.tokens.push(numberStr);
    tokenStream.types.push(this.NUM);

    previousNumberEndIndex=numberEndIndex;
    firstIteration=false;
  }
  if (numberEndIndex<origText.length) { // process any remaining text
    this.tokeniseNonNumbers(tokenStream,origText.slice(numberEndIndex));
  }


  // run through the tokens, if there is a number beside text (not a symbol, then mark it as text too)
  // exmaples: 3d or d3 - 3 is not a number here
  var tokens=tokenStream.tokens;
  var types=tokenStream.types;
  var i;
  
  var numJoinedToText;
  var matchBoundary = VMC.CurrencyList.prototype.matchBoundary;
  var matchStartBoundaryRegExp = new RegExp('^'+matchBoundary);
  var matchEndBoundaryRegExp   = new RegExp(matchBoundary+'$');
  for (i=0;i<tokens.length;i++) {
    if (this.findToken(this.NUM,i,types,0)) {
      numJoinedToText=false;
      if (this.findToken(this.TEXT,i,types,-1)) {
	if (!matchEndBoundaryRegExp.test(tokens[i-1])) { numJoinedToText=true; } // if the text token doesn't end with boundary char
      }
      //WILL: this breaks things like $55bn    - REMOVED
//       if (this.findToken(this.TEXT,i,types,+1)) {
// 	if (!matchStartBoundaryRegExp.test(tokens[i+1])) { numJoinedToText=true; } // if the text token doesn't start with boundary char
//       }
      if (numJoinedToText) {
	types[i]=this.TEXT; //change the number into text
      }
    }
  }
  
  if (VMC.debugLevel>0) {
    var debugStr='';
    for (i=0;i<tokens.length;i++) {
      if (this.findToken(this.NUM,i,types,0)) { debugStr += tokens[i]+', '; }
    }
    if (debugStr) { VMC.log('DBG','Found numbers: '+debugStr) }
    else          { VMC.log('DBG','Found no numbers') }
  }
  
  return tokenStream;
},

//WILL: TODO: make tokenStream an object and this a method of it
findToken : function(type,start,types,offset) {
    var index=start+offset;
    if (index<0 || index>=types.length) { return false; }
    return (types[index]==type);
  },
  findTokenSpaceLen : function(start,types,offset,tokens) {
    if (this.findToken(this.SPACE,start,types,offset)) { return tokens[start+offset].length; }
    else { return null; }
  },
  
pushToken : function(stream,type,value) {
  stream.tokens.push(value);
  stream.types.push(type);
},

// search start and end of 'str' for whitespace and currency symbols //NB doesn't search middle if it's a big string
// - inserts TEXT, SYMBOL and SPACE tokens
tokeniseNonNumbers : function(tokenStream,str,firstIteration) {
  if (!str) { return; }
  //debug('tokenising ['+str+']');
  var r;
  var spaceStr; //whitespace
  var matchedStr, boundaryStr, currencyStr;

  if (!firstIteration) {
    //    var leftWindow = str.substr(0,this.WINDOW_SIZE); //WILL: TODO: optimise by only searching a substring?
    r = this.currencyRightRegExp2.exec(str); // currency symbol to the right of digits e.g. 1 USD
    if (r) {
      spaceStr    =r[1]; //space between symbol and digit
      currencyStr =r[2]; //currency symbol
      boundaryStr =r[3]; //char before start of currency symbol
      
      if (spaceStr) { this.pushToken(tokenStream,this.SPACE,spaceStr); }
      this.pushToken(tokenStream,this.SYMBOL,currencyStr);     
      str = boundaryStr + RegExp.rightContext;
    }
    if (!str) { return; }
  }

  // IGNORE the middle part

  r = this.currencyLeftRegExp2.exec(str); // currency symbol is to the left of the digits e.g. $1
  if (r) {
    boundaryStr =r[1]; //char before start of currency symbol
    currencyStr =r[2]; //currency symbol
    spaceStr    =r[3]; //space between symbol and digit

    str = RegExp.leftContext + boundaryStr;

    if (str) { this.pushToken(tokenStream,this.TEXT,str); }
    this.pushToken(tokenStream,this.SYMBOL,currencyStr);
    if (spaceStr) { this.pushToken(tokenStream,this.SPACE,spaceStr); }

      
  } else {
    // there was no currency in the right hand side - so push the remaining str as TEXT or WHITESPACE
    if ( /\S/.test(str)) { this.pushToken(tokenStream,this.TEXT,str); }
    else {                 this.pushToken(tokenStream,this.SPACE,str); }
  }

},

    
associateNumbers : function(tokenStream) {

  var tokens=tokenStream.tokens;
  var types =tokenStream.types;
  var symbol2number=[];
  var number2symbol=[]; // [numberPos,symbolPos,spaceChars]
  var space;
  var i;


  //for (i=0;i<tokens.length;i++) { number2symbol[i]=[]; }

  for (i=0;i<tokens.length;i++) {
    //debug('token:'+tokens[i]+' '+types[i]);
    if (!this.findToken(this.NUM,i,types,0)) { continue; } //skip non-numbers

    for (var dir=-1; dir<=1; dir+=2) { //look before and after current number
      if (this.findToken(this.SYMBOL,i,types,dir)) { number2symbol.push([i, (i+dir) , 0] ); }
      else {
	space = this.findTokenSpaceLen(i,types,dir,tokens);
	if (space && this.findToken(this.SYMBOL,i,types,dir*2)) { number2symbol.push([ i, (i+dir*2) , space]); }
      }
    }
  }

  for (var sortByIndex=0; sortByIndex<=1; sortByIndex++) {
    // find the shortest link for each num
    var minDist=[];
    var num;
    var dist;
    for (i=0;i<number2symbol.length;i++) {
      num=number2symbol[i][sortByIndex];      dist=number2symbol[i][2];
      if (minDist[num]===undefined || minDist[num]>dist) { minDist[num]=dist; }
    }
      
    var dups=[];
    // mark all but the shortest as unused, check for duplicates
    for (i=0;i<number2symbol.length;i++) {
      num=number2symbol[i][sortByIndex];   dist=number2symbol[i][2];
      if (!dups[num]) {dups[num]=0;} //initialise hashtable
	
      if (dist != minDist[num]) { number2symbol[i][3]=true; }
      else                      { dups[num]++; }
    }
      
    for (i=0;i<number2symbol.length;i++) {
      num=number2symbol[i][sortByIndex];
      if (dups[num]>1) { // we found 2 links at minimum distance. This number is ambigious. Mark as unused
	number2symbol[i][3]=true;
      }
    }
  }

  var number2symbolOK=[];
  for (i=0;i<number2symbol.length;i++) {
    if (!number2symbol[i][3]) {
      number2symbolOK.push( number2symbol[i][0]);
      number2symbolOK.push( number2symbol[i][1]);
    }
  }

  //     for (i=0;i<number2symbolOK.length;i++) {
  //       info('number2symbolOK['+i+']='+number2symbolOK[i]);
  //     }

  return number2symbolOK;

},




// this is also used by the tooltip code
createTokenStream : function(text,conversionProfile) {
    this.currencyLeftRegExp2 = conversionProfile.getLeftRE(); //setup the global vars used by tokenise
    this.currencyRightRegExp2 = conversionProfile.getRightRE();
    return this.tokeniseNumbers(text);
  },

//WILL: TODO: this duplicates a lot of code in currencyConvertStr, should be merged if that doesn't hurt performance
currencyConvertStrForTooltip : function(origText, targetCurrency, conversionProfile, getRateFunc) { //the new version
  var tokenStream=this.createTokenStream(origText,conversionProfile);
  var tokens=tokenStream.tokens;
  var types=tokenStream.types;

  var numberAssociation = this.associateNumbers(tokenStream);    
  var numberPos;
  var symbolPos;
  var numberStr;
  var symbolStr;


  if (numberAssociation.length==0) { return null; }
  //WILL: TODO: now we just take the first symbol+number, should we look at the others and find the one closest to the mouse?
  var i=0;
  numberPos=numberAssociation[i+0];
  symbolPos=numberAssociation[i+1];
  numberStr=tokens[numberPos];
  symbolStr=tokens[symbolPos];

  if (this.isSpecialException(symbolStr,numberStr)) { return null; } //skip this conversion
  var sourceCurrency=conversionProfile.findCurrencyFromStr(symbolStr);

  var convertedText = '';
  if (targetCurrency) {
    var rate=getRateFunc(sourceCurrency,targetCurrency);
    var number = new VMC.Number(numberStr);
    number.multiply(rate);
    var newNumberStr=number.toString();
    convertedText = targetCurrency.getHtml()+' '+newNumberStr;  //WILL: TODO: respect symbol before or after setting for currencyConvertStrForTooltip
  }

  var result = { convertedText:convertedText, originalText:symbolStr+' '+numberStr,
		 convertedCurrency:targetCurrency.description, originalCurrency:sourceCurrency.description };
  return result;
  },

 //conversions to skip, E.g.  'Euro2006'
  isSpecialException : function(symbolStr,numberStr) {
  
    if (symbolStr.toLowerCase() == 'euro') {
      // Skip 'Euro 2006'  or '1990 Euro games'
      if (numberStr.length == 4) { 
	var century = numberStr.substr(0,2);
	if (century == '19' || century == '20') {
	  return true;
	}
      }
      // Skip 'Euro81'  'Euro06'  'Euro21'  and also '90Euro'  // WILL: consider if the last isn't a problem
      if (numberStr.length == 2) {
	var year = parseInt(numberStr);
	if (year<30 || year>80) {
	  return true;
	}
      }
    }
    return false;
  },

//WILL: TODO: DOCS: symbol means currencySymbol
currencyConvertStr : function(origText, targetCurrency, conversionProfile, getRateFunc, hideOriginal, defaultSourceCurr) { //the new version
  var missingRates=[]; // have we already complained about a problem with this rate?
  //info('NEW_CurrencyConvertStr: ['+origText+']');
  var tokenStream=this.createTokenStream(origText,conversionProfile);
  var tokens=tokenStream.tokens;
  var types=tokenStream.types;

  //hashtable of numberPositions that have already been converted. Used to convert any numbers without symbols to default curr
  var numberWasConverted=[]; 

  if (!targetCurrency) { throw('missing target currency'); }

  var numberAssociation = this.associateNumbers(tokenStream);    
  //info('numberAssociation='+numberAssociation);
  var numberPos;
  var symbolPos;
  var numberStr;
  var symbolStr;
  var firstPos;
  var secondPos;
  var firstStr;
  var secondStr;
  var originalStr;
  var spaceBetween;
  var newNumberStr;
  var numberBeforeSymbol;
  var rate;
  var sourceCurrency;
  var spaceStr;
  var nbsp='\u00A0'; // non-breaking space so that currency symbol and number remain together

  var madeChanges = false; // was the text modified

  for (var i=0;i<numberAssociation.length;i+=2) {
    numberPos=numberAssociation[i+0];
    symbolPos=numberAssociation[i+1];
    numberStr=tokens[numberPos];
    symbolStr=tokens[symbolPos];
    //info('converting '+symbolStr+numberStr);

    if (this.isSpecialException(symbolStr,numberStr)) continue; //skip this conversion

    sourceCurrency=conversionProfile.findCurrencyFromStr(symbolStr,true); //fail silently
    if (!sourceCurrency) { 
      VMC.log('WARN','Currency symbol ['+symbolStr+'] is recognised in the RegExp list, but it is not in the conversionProfile');
      continue; //skip this one
    }

    numberWasConverted[numberPos]=true; // if we recognised this as a money then mark it as converted. It should not be considered for default conversion

    if (sourceCurrency.equals(targetCurrency)) { continue; } // skip this money, it's already in the target currency

    firstPos  = Math.min(symbolPos,numberPos);
    secondPos = Math.max(symbolPos,numberPos);
    if      ((secondPos-firstPos)==1) { spaceBetween=false; }
    else if ((secondPos-firstPos)==2) { spaceBetween=true;  }
    else { throw 'WILL: number and symbol are not 1 or 2 tokens apart. numberPos='+numberPos+' symbolPos='+symbolPos; }

    if (spaceBetween) { spaceStr=nbsp; } //this only used for (original_value)
    else              { spaceStr='';  }

    numberBeforeSymbol = (numberPos==firstPos); // is number first or second?

    var number; //WILL: TODO: this is only used inside else {}
    if (missingRates[ sourceCurrency+'-'+targetCurrency]) {
      newNumberStr='???'; // we already had a problem with this rate. Don't complain multiple times
    } else {
      rate=null;
      try {
	rate=getRateFunc(sourceCurrency,targetCurrency);
      } catch (e) {}
      if (!VMC.ExchangeRate.isSaneValue(rate)) { 
	VMC.log('INFO', 'Failed to convert text as '+sourceCurrency+'/'+targetCurrency+' missing or invalid in ExchangeRateCache'); //WILL: TODO: show in the GUI . (Use INFO as WARN gives too much stacktrace)
	//WILL: TODO: decide if previous technique of return origText gave better user experience.
	newNumberStr='???';
	missingRates[ sourceCurrency+'-'+targetCurrency] = true;
      } else {
	number = new VMC.Number(numberStr);
	number.multiply(rate);
	newNumberStr=number.toString();
      }
    }
  
    if (numberBeforeSymbol) { originalStr = numberStr+spaceStr+symbolStr; }
    else                    { originalStr = symbolStr+spaceStr+numberStr; }


    // I try to respect the order of the original page (otherwise tables might look strange if just a few currencies are converted)
    // but some users want to force the order of currency symbol and number:
    switch (this.forceSymbolPosition) {
    case 'FIRST' : numberBeforeSymbol=false; break;
    case 'LAST' :  numberBeforeSymbol=true; break;
    case 'RESPECT' : break; // don't change the detected position
    default : VMC.log('WARN','Unknown forceSymbolPosition '+this.forceSymbolPosition);
    }

    if (numberBeforeSymbol) { firstStr = newNumberStr;             secondStr = targetCurrency.getHtml(); }
    else                    { firstStr = targetCurrency.getHtml(); secondStr = newNumberStr;             }



    // normally I respect exsiting formatting, but user can force a space
    var forcedSpace='';
    if (this.forceSpaceBetweenNumberAndSymbol) {
      if (!spaceBetween) {
	// there is no intervening whitespace
	forcedSpace = nbsp; // so force one (non-breaking)
      }
    }

    if ( ! this.targetInBrackets) {
      originalStr = ' ('+originalStr+')'; // default is to show source currency in brackets (space before)
    } else {                              // else they want the target currency in brackets
      firstStr = ' (' + firstStr;         // (space before) Spaces required because of the order below:
      secondStr =      secondStr + ')'; 
    }

    if (hideOriginal) { originalStr=''; }

    // store the converted values back into the token stream
    // note that there could be one whitespace token between firstPos and secondPos
    if ( ! this.targetInBrackets) { // order is target (source) : Default 
      tokens[firstPos]  = firstStr + forcedSpace;
      tokens[secondPos] = secondStr + originalStr;
    } else {                                           // order is source (target)
      tokens[firstPos]  = originalStr;     // WILL: TODO: extra whitespace can appear here!
      tokens[secondPos] = firstStr + forcedSpace + secondStr;
    } 
    madeChanges = true;

  }

  // WILL: TODO: add missing rate warning minimilisation from above?? TODO: add targetInBrackets handling

  if (defaultSourceCurr) {
    sourceCurrency=defaultSourceCurr;
    // go through and find unconverted numbers and use default source currency to convert them
    for (i=0;i<tokens.length;i++) {
      if (types[i]==this.NUM && !numberWasConverted[i]) {
	numberStr = tokens[i];
	//WILL: TODO: this code is repeated from about. Should be func? Or would that slow down?
	rate=null;
	try {
	  rate=getRateFunc(sourceCurrency,targetCurrency);
	} catch (e) {}
	if (!VMC.ExchangeRate.isSaneValue(rate)) { 
	  VMC.log('WARN', 'Failed to convert text as '+sourceCurrency+'/'+targetCurrency+' missing or invalid in ExchangeRateCache'); //WILL: TODO: show in GUI
	  newNumberStr='???';
	} else {
	  number = new VMC.Number(numberStr);
	  number.multiply(rate);
	  newNumberStr=number.toString();
	}

	if (hideOriginal) { originalStr=''; }
	else              { originalStr = ' ('+sourceCurrency.getHtml()+numberStr+')'; } //WILL: TODO: should we use nbsp?
	tokens[i]=targetCurrency.getHtml()+newNumberStr+originalStr;
	madeChanges = true;
	// (we do not add spaces between currency and number to minimise the extra chars we are adding)

	// don't need to mark numberWasConverted
      }
    }
  }

  if (madeChanges) {
    //info('NEW_CurrencyConvertStr --> ['+tokens.join('')+']');
    return tokens.join('');
  } else {
    return null; // text does not need to be modified
  }

},

// I try to respect the order of the original page (otherwise tables might look strange if just a few currencies are converted)
// but some users want to force the order of currency symbol and number:
  forceSymbolPosition : 'RESPECT', // or 'FIRST' or 'LAST'

// true:  source value in brackets (default)
// false: target value in brackets
  targetInBrackets : false,

// normally I respect exsiting formatting, but user can force a space
  forceSpaceBetweenNumberAndSymbol : false

};

